Mestr JavaScript Promise combinators (Promise.all, Promise.allSettled, Promise.race, Promise.any) for effektiv og robust asynkron programmering i globale applikationer.
JavaScript Promise Combinators: Avancerede asynkrone mønstre for globale applikationer
Asynkron programmering er en hjørnesten i moderne JavaScript, især når man bygger webapplikationer, der interagerer med API'er, databaser eller udfører tidskrævende operationer. JavaScript Promises giver en kraftfuld abstraktion til at håndtere asynkrone operationer, men at mestre dem kræver en forståelse af avancerede mønstre. Denne artikel dykker ned i JavaScript Promise combinators – Promise.all, Promise.allSettled, Promise.race og Promise.any – og hvordan de kan bruges til at skabe effektive og robuste asynkrone arbejdsgange, især i forbindelse med globale applikationer med varierende netværksforhold og datakilder.
Forståelse af Promises: En hurtig opsummering
Før vi dykker ned i combinators, lad os hurtigt gennemgå Promises. Et Promise repræsenterer det endelige resultat af en asynkron operation. Det kan være i en af tre tilstande:
- Afventende: Den indledende tilstand, hverken opfyldt eller afvist.
- Opfyldt: Handlingen blev gennemført succesfuldt, med en resulterende værdi.
- Afvist: Handlingen mislykkedes, med en årsag (normalt et Error-objekt).
Promises tilbyder en renere og mere håndterbar måde at håndtere asynkrone operationer på sammenlignet med traditionelle callbacks. De forbedrer kodens læsbarhed og forenkler fejlhåndtering. Vigtigst af alt danner de også grundlaget for de Promise combinators, vi vil udforske.
Promise Combinators: Orkestrering af asynkrone operationer
Promise combinators er statiske metoder på Promise-objektet, der giver dig mulighed for at administrere og koordinere flere Promises. De tilbyder kraftfulde værktøjer til at bygge komplekse asynkrone arbejdsgange. Lad os undersøge hver enkelt i detaljer.
Promise.all(): Udførelse af Promises parallelt og aggregering af resultater
Promise.all() tager en itererbar (normalt et array) af Promises som input og returnerer et enkelt Promise. Dette returnerede Promise opfyldes, når alle input-Promises er blevet opfyldt. Hvis et af input-Promises afvises, afvises det returnerede Promise øjeblikkeligt med årsagen fra det første afviste Promise.
Anvendelseseksempel: Når du skal hente data fra flere API'er samtidigt og behandle de kombinerede resultater, er Promise.all() ideel. Forestil dig for eksempel at bygge et dashboard, der viser vejrinformation fra forskellige byer rundt om i verden. Hver bys data kunne hentes via et separat API-kald.
async function fetchWeatherData(city) {
try {
const response = await fetch(`https://api.example.com/weather?city=${city}`); // Erstat med et rigtigt API-endepunkt
if (!response.ok) {
throw new Error(`Kunne ikke hente vejrdata for ${city}`);
}
return await response.json();
} catch (error) {
console.error(`Fejl ved hentning af vejrdata for ${city}: ${error}`);
throw error; // Genkast fejlen, så den fanges af Promise.all
}
}
async function displayWeatherData() {
const cities = ['London', 'Tokyo', 'New York', 'Sydney'];
try {
const weatherDataPromises = cities.map(city => fetchWeatherData(city));
const weatherData = await Promise.all(weatherDataPromises);
weatherData.forEach((data, index) => {
console.log(`Vejret i ${cities[index]}:`, data);
// Opdater brugergrænsefladen med vejrdataene
});
} catch (error) {
console.error('Kunne ikke hente vejrdata for alle byer:', error);
// Vis en fejlmeddelelse til brugeren
}
}
displayWeatherData();
Overvejelser for globale applikationer:
- Netværkslatens: Anmodninger til forskellige API'er på forskellige geografiske placeringer kan opleve varierende latens.
Promise.all()garanterer ikke den rækkefølge, hvori Promises opfyldes, kun at de alle opfyldes (eller én afvises), før det kombinerede Promise afgøres. - API Rate Limiting: Hvis du foretager flere anmodninger til det samme API eller flere API'er med delte rate limits, kan du overskride disse grænser. Implementer strategier som at sætte anmodninger i kø eller bruge eksponentiel backoff for at håndtere rate limiting elegant.
- Fejlhåndtering: Husk, at hvis blot ét Promise afvises, fejler hele
Promise.all()-operationen. Dette er muligvis ikke ønskeligt, hvis du vil vise delvise data, selvom nogle anmodninger mislykkes. Overvej at brugePromise.allSettled()i sådanne tilfælde (forklaret nedenfor).
Promise.allSettled(): Håndtering af succes og fiasko individuelt
Promise.allSettled() ligner Promise.all(), men med en afgørende forskel: den venter på, at alle input-Promises afgøres, uanset om de opfyldes eller afvises. Det returnerede Promise opfyldes altid med et array af objekter, der hver især beskriver resultatet af det tilsvarende input-Promise. Hvert objekt har en status-egenskab (enten "fulfilled" eller "rejected") og en value (hvis opfyldt) eller reason (hvis afvist) egenskab.
Anvendelseseksempel: Når du skal indsamle resultater fra flere asynkrone operationer, og det er acceptabelt, at nogle mislykkes uden at få hele operationen til at fejle, er Promise.allSettled() det bedre valg. Forestil dig et system, der behandler betalinger gennem flere betalingsgateways. Du vil måske forsøge alle betalinger og registrere, hvilke der lykkedes, og hvilke der mislykkedes.
async function processPayment(paymentGateway, amount) {
try {
const response = await paymentGateway.process(amount); // Erstat med en rigtig betalingsgateway-integration
if (response.status === 'success') {
return { status: 'fulfilled', value: `Betaling behandlet succesfuldt via ${paymentGateway.name}` };
} else {
throw new Error(`Betaling mislykkedes via ${paymentGateway.name}: ${response.message}`);
}
} catch (error) {
return { status: 'rejected', reason: `Betaling mislykkedes via ${paymentGateway.name}: ${error.message}` };
}
}
async function processMultiplePayments(paymentGateways, amount) {
const paymentPromises = paymentGateways.map(gateway => processPayment(gateway, amount));
const results = await Promise.allSettled(paymentPromises);
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(result.value);
} else {
console.error(result.reason);
}
});
// Analyser resultaterne for at bestemme den samlede succes/fiasko
const successfulPayments = results.filter(result => result.status === 'fulfilled').length;
const failedPayments = results.filter(result => result.status === 'rejected').length;
console.log(`Succesfulde betalinger: ${successfulPayments}`);
console.log(`Mislykkede betalinger: ${failedPayments}`);
}
// Eksempel på betalingsgateways
const paymentGateways = [
{ name: 'PayPal', process: (amount) => Promise.resolve({ status: 'success', message: 'Betaling succesfuld' }) },
{ name: 'Stripe', process: (amount) => Promise.reject({ status: 'error', message: 'Utilstrækkelige midler' }) },
{ name: 'Worldpay', process: (amount) => Promise.resolve({ status: 'success', message: 'Betaling succesfuld' }) },
];
processMultiplePayments(paymentGateways, 100);
Overvejelser for globale applikationer:
- Robusthed:
Promise.allSettled()forbedrer robustheden af dine applikationer ved at sikre, at alle asynkrone operationer forsøges, selvom nogle mislykkes. Dette er især vigtigt i distribuerede systemer, hvor fejl er almindelige. - Detaljeret rapportering: Resultatarrayet giver detaljerede oplysninger om hver operations udfald, hvilket giver dig mulighed for at logge fejl, genforsøge mislykkede operationer eller give brugerne specifik feedback.
- Delvis succes: Du kan nemt bestemme den samlede succesrate og træffe passende foranstaltninger baseret på antallet af succesfulde og mislykkede operationer. For eksempel kan du tilbyde alternative betalingsmetoder, hvis den primære gateway fejler.
Promise.race(): Valg af det hurtigste resultat
Promise.race() tager også en itererbar af Promises som input og returnerer et enkelt Promise. Men i modsætning til Promise.all() og Promise.allSettled(), afgøres Promise.race() så snart et hvilket som helst af input-Promises afgøres (enten opfyldes eller afvises). Det returnerede Promise opfyldes eller afvises med værdien eller årsagen fra det først afgjorte Promise.
Anvendelseseksempel: Når du skal vælge det hurtigste svar fra flere kilder, er Promise.race() et godt valg. Forestil dig at forespørge flere servere for de samme data og bruge det første svar, du modtager. Dette kan forbedre ydeevnen og responsiviteten, især i situationer, hvor nogle servere midlertidigt kan være utilgængelige eller langsommere end andre.
async function fetchDataFromServer(serverURL) {
try {
const response = await fetch(serverURL, {signal: AbortSignal.timeout(5000)}); //Tilføj en timeout på 5 sekunder
if (!response.ok) {
throw new Error(`Kunne ikke hente data fra ${serverURL}`);
}
return await response.json();
} catch (error) {
console.error(`Fejl ved hentning af data fra ${serverURL}: ${error}`);
throw error;
}
}
async function getFastestResponse() {
const serverURLs = [
'https://server1.example.com/data', // Erstat med rigtige server-URL'er
'https://server2.example.com/data',
'https://server3.example.com/data',
];
try {
const dataPromises = serverURLs.map(serverURL => fetchDataFromServer(serverURL));
const fastestData = await Promise.race(dataPromises);
console.log('Hurtigste data modtaget:', fastestData);
// Brug de hurtigste data
} catch (error) {
console.error('Kunne ikke hente data fra nogen server:', error);
// Håndter fejlen
}
}
getFastestResponse();
Overvejelser for globale applikationer:
- Timeouts: Det er afgørende at implementere timeouts, når du bruger
Promise.race(), for at forhindre det returnerede Promise i at vente uendeligt, hvis nogle af input-Promises aldrig afgøres. Eksemplet ovenfor bruger `AbortSignal.timeout()` for at opnå dette. - Netværksforhold: Den hurtigste server kan variere afhængigt af brugerens geografiske placering og netværksforhold. Overvej at bruge et Content Delivery Network (CDN) til at distribuere dit indhold og forbedre ydeevnen for brugere over hele verden.
- Fejlhåndtering: Hvis det Promise, der 'vinder' kapløbet, afvises, så afvises hele Promise.race. Sørg for, at hvert Promise har passende fejlhåndtering for at forhindre uventede afvisninger. Også, hvis det "vindende" promise afvises på grund af en timeout (som vist ovenfor), vil de andre promises fortsætte med at køre i baggrunden. Du skal muligvis tilføje logik for at annullere disse andre promises ved hjælp af `AbortController`, hvis de ikke længere er nødvendige.
Promise.any(): Accepter den første opfyldelse
Promise.any() ligner Promise.race(), men med en lidt anderledes adfærd. Den venter på, at det første input-Promise bliver opfyldt. Hvis alle input-Promises afvises, afviser Promise.any() med en AggregateError, der indeholder et array af afvisningsårsagerne.
Anvendelseseksempel: Når du skal hente data fra flere kilder, og du kun er interesseret i det første succesfulde resultat, er Promise.any() et godt valg. Dette er nyttigt, når du har redundante datakilder eller alternative API'er, der giver de samme oplysninger. Det prioriterer succes over hastighed, da det venter på den første opfyldelse, selvom nogle Promises afvises hurtigt.
async function fetchDataFromSource(sourceURL) {
try {
const response = await fetch(sourceURL);
if (!response.ok) {
throw new Error(`Kunne ikke hente data fra ${sourceURL}`);
}
return await response.json();
} catch (error) {
console.error(`Fejl ved hentning af data fra ${sourceURL}: ${error}`);
throw error;
}
}
async function getFirstSuccessfulData() {
const dataSources = [
'https://source1.example.com/data', // Erstat med rigtige datakilde-URL'er
'https://source2.example.com/data',
'https://source3.example.com/data',
];
try {
const dataPromises = dataSources.map(sourceURL => fetchDataFromSource(sourceURL));
const data = await Promise.any(dataPromises);
console.log('Første succesfulde data modtaget:', data);
// Brug de succesfulde data
} catch (error) {
if (error instanceof AggregateError) {
console.error('Kunne ikke hente data fra nogen kilde:', error.errors);
// Håndter fejlen
} else {
console.error('Der opstod en uventet fejl:', error);
}
}
}
getFirstSuccessfulData();
Overvejelser for globale applikationer:
- Redundans:
Promise.any()er særligt nyttig, når man arbejder med redundante datakilder, der giver lignende oplysninger. Hvis en kilde er utilgængelig eller langsom, kan du stole på, at de andre leverer dataene. - Fejlhåndtering: Sørg for at håndtere den
AggregateError, der kastes, når alle input-Promises afvises. Denne fejl indeholder et array af de individuelle afvisningsårsager, hvilket giver dig mulighed for at fejlfinde og diagnosticere problemerne. - Prioritering: Den rækkefølge, du giver Promises til
Promise.any(), har betydning. Placer de mest pålidelige eller hurtigste datakilder først for at øge sandsynligheden for et succesfuldt resultat.
Valg af den rette Combinator: En opsummering
Her er en hurtig opsummering, der kan hjælpe dig med at vælge den passende Promise combinator til dine behov:
- Promise.all(): Brug, når du har brug for, at alle Promises opfyldes succesfuldt, og du ønsker at fejle øjeblikkeligt, hvis et Promise afvises.
- Promise.allSettled(): Brug, når du vil vente på, at alle Promises afgøres, uanset succes eller fiasko, og du har brug for detaljerede oplysninger om hvert udfald.
- Promise.race(): Brug, når du vil vælge det hurtigste resultat fra flere Promises, og du kun er interesseret i det første, der afgøres.
- Promise.any(): Brug, når du vil acceptere det første succesfulde resultat fra flere Promises, og det ikke gør noget, hvis nogle Promises afvises.
Avancerede mønstre og bedste praksis
Ud over den grundlæggende brug af Promise combinators er der flere avancerede mønstre og bedste praksis, man skal huske på:
Begrænsning af samtidighed
Når du arbejder med et stort antal Promises, kan det overvælde dit system eller overskride API rate limits at udføre dem alle parallelt. Du kan begrænse samtidigheden ved hjælp af teknikker som:
- Chunking: Opdel dine Promises i mindre bidder og behandl hver bid sekventielt.
- Brug af en Semafor: Implementer en semafor for at kontrollere antallet af samtidige operationer.
Her er et eksempel, der bruger chunking:
async function processInChunks(promises, chunkSize) {
const results = [];
for (let i = 0; i < promises.length; i += chunkSize) {
const chunk = promises.slice(i, i + chunkSize);
const chunkResults = await Promise.all(chunk);
results.push(...chunkResults);
}
return results;
}
// Eksempel på brug
const myPromises = [...Array(100)].map((_, i) => Promise.resolve(i)); //Opret 100 promises
processInChunks(myPromises, 10) // Behandler 10 promises ad gangen
.then(results => console.log('Alle promises er løst:', results));
Håndtering af fejl på en elegant måde
Korrekt fejlhåndtering er afgørende, når man arbejder med Promises. Brug try...catch-blokke til at fange fejl, der kan opstå under asynkrone operationer. Overvej at bruge biblioteker som p-retry eller retry til automatisk at genforsøge mislykkede operationer.
async function fetchDataWithRetry(url, retries = 3) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP-fejl! status: ${response.status}`);
}
return await response.json();
} catch (error) {
if (retries > 0) {
console.log(`Prøver igen om 1 sekund... (Forsøg tilbage: ${retries})`);
await new Promise(resolve => setTimeout(resolve, 1000)); // Vent 1 sekund
return fetchDataWithRetry(url, retries - 1);
} else {
console.error('Maksimalt antal forsøg nået. Operationen mislykkedes.');
throw error;
}
}
}
Brug af Async/Await
async og await giver en mere synkron-lignende måde at arbejde med Promises på. De kan forbedre kodens læsbarhed og vedligeholdelighed markant.
Husk at bruge try...catch-blokke omkring await-udtryk for at håndtere potentielle fejl.
Annullering
I nogle scenarier kan det være nødvendigt at annullere ventende Promises, især når man har med langvarige operationer eller bruger-initierede handlinger at gøre. Du kan bruge AbortController API'et til at signalere, at et Promise skal annulleres.
const controller = new AbortController();
const signal = controller.signal;
async function fetchDataWithCancellation(url) {
try {
const response = await fetch(url, { signal });
if (!response.ok) {
throw new Error(`HTTP-fejl! status: ${response.status}`);
}
return await response.json();
} catch (error) {
if (error.name === 'AbortError') {
console.log('Fetch afbrudt');
} else {
console.error('Fejl ved hentning af data:', error);
}
throw error;
}
}
fetchDataWithCancellation('https://api.example.com/data')
.then(data => console.log('Data modtaget:', data))
.catch(error => console.error('Fetch mislykkedes:', error));
// Annuller fetch-operationen efter 5 sekunder
setTimeout(() => {
controller.abort();
}, 5000);
Konklusion
JavaScript Promise combinators er kraftfulde værktøjer til at bygge robuste og effektive asynkrone applikationer. Ved at forstå nuancerne i Promise.all, Promise.allSettled, Promise.race og Promise.any, kan du orkestrere komplekse asynkrone arbejdsgange, håndtere fejl elegant og optimere ydeevnen. Når man udvikler globale applikationer, er det afgørende at overveje netværkslatens, API rate limits og datakilders pålidelighed. Ved at anvende de mønstre og bedste praksis, der er diskuteret i denne artikel, kan du skabe JavaScript-applikationer, der er både ydedygtige og modstandsdygtige, og som leverer en overlegen brugeroplevelse til brugere over hele verden.